package com.zenika.zenbinding.client.model;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.google.gwt.user.client.ui.Widget;
import com.zenika.zenbinding.client.converter.Converter;
import com.zenika.zenbinding.client.exception.ConverterException;
import com.zenika.zenbinding.client.exception.DataException;
import com.zenika.zenbinding.client.exception.DataMessage;
import com.zenika.zenbinding.client.exception.ValidatorException;
import com.zenika.zenbinding.client.feedback.FeedbackManager;
import com.zenika.zenbinding.client.handler.Handler;
import com.zenika.zenbinding.client.handler.ValueBindingListener;
import com.zenika.zenbinding.client.util.BeanUtil;
import com.zenika.zenbinding.client.validator.ValidationContext;
import com.zenika.zenbinding.client.validator.Validator;

/**
 * A ValueModel manages the access to the value of a BusinessObject, the
 * conversion to and from String, the validation and redirect errors of
 * Validation or Conversion to a FeedbackManager.
 * 
 * @author Zenika
 */
public class ValueModel implements Model, ValueBindingListener {

	private Object simpleSource;
	private BusinessObject source;
	private String propertyPath;
	private Converter converter;
	private List validators;
	private FeedbackManager feedback;
	private boolean errorState;
	private DataMessage currentError;
	private Handler handler;
	private String feedbackId;

	/**
	 * Build a ValueModel. Constructor to use if the source object is not the
	 * property to access.
	 * 
	 * @param source
	 *            The object containing the data
	 * @param propertyPath
	 *            The path to access the property
	 */
	public ValueModel(BusinessObject source, String propertyPath) {
		this.source = source;
		this.propertyPath = propertyPath;
		this.validators = new ArrayList();
	}

	/**
	 * Build a ValueModel. Use this contructor if the property you want to
	 * access is the given source object.
	 * 
	 * @param source
	 *            The property to bind
	 */
	public ValueModel(Object source) {
		this.simpleSource = source;
	}

	/**
	 * Specify the converter to use with this model. If you don't do it, a
	 * converter is automatically selected.
	 * 
	 * @param converter
	 *            Converter
	 */
	public void setConverter(Converter converter) {
		this.converter = converter;
	}

	/**
	 * Set the feedback manager to notify in case of validation or convertion
	 * error.
	 * 
	 * @param feedback
	 *            FeedbackManager
	 */
	public void setFeedbackManager(FeedbackManager feedback) {
		this.feedback = feedback;
	}

	/**
	 * Add a validator to the model.
	 * 
	 * @param validator
	 *            Validator
	 */
	public void addValidator(Validator validator) {
		this.validators.add(validator);
	}

	/**
	 * Add a validator to the model, using an existing context.
	 * 
	 * @param validator
	 *            Validator
	 */
	public void addValidator(ValidationContext context, Validator validator) {
		if (validator != null)
			this.validators.add(validator);
		context.addModel(this);
	}

	/**
	 * Converts the property managed by the ValueModel into String and returns
	 * it.
	 * 
	 * @return String representation of the ValueModel
	 */
	public String getValue() {
		Object value = null;
		if (this.simpleSource != null) {
			value = this.simpleSource;
		} else {
			try {
				value = BeanUtil.getProperty(this.source, this.propertyPath);
			} catch (DataException e1) {
				e1.printStackTrace();
			}
		}
		String result = null;
		try {
			result = this.converter.fromObjectToString(value);
		} catch (ConverterException e) {
			e.printStackTrace();
		}
		return result;
	}

	/**
	 * Update the ValueModel from a string. Before the update, the string is
	 * converted into object and validate. If there is any error in validation
	 * or conversion, the ValueModel go into the error state.
	 * 
	 * @param string
	 *            New value
	 * @param handler
	 *            Handler
	 */
	public void updateModel(String string, Handler handler) {
		try {
			Object result = this.converter.fromStringToObject(string);
			this.validate(result);

			// validation succeeded, leave errorState if it were in
			if (this.errorState) {
				this.errorState = false;
				this.currentError = null;
				this.fireErrorStop(handler);
			}
			if (this.simpleSource != null) {
				this.simpleSource = result;
			} else {
				BeanUtil.setProperty(this.source, this.propertyPath, result);
			}

		} catch (ConverterException e) {
			if (!this.errorState) {
				this.errorState = true;
				this.currentError = e.getDataMessage();
				this.fireError(this.currentError, handler);
			}
		} catch (ValidatorException e) {
			if (!this.errorState) {
				this.errorState = true;
				this.currentError = e.getDataMessage();
				this.fireError(this.currentError, handler);
			}
		}
	}

	/**
	 * Validate the object. If the validation fails, a ValidatorException is
	 * thrown.
	 * 
	 * @param object
	 *            Object to validate
	 * @throws ValidatorException
	 */
	private void validate(Object object) throws ValidatorException {
		Iterator iter = this.validators.iterator();
		while (iter.hasNext()) {
			((Validator) iter.next()).validate(object);
		}
	}

	/**
	 * Called when there is a change in the displayed value.
	 */
	public void onChange(String newValue, Handler handler) {
		this.updateModel(newValue, handler);
	}

	/**
	 * Get the object containing the property managed by the value model or
	 * directly the property if the propertyPath is null.
	 * 
	 * @return Object
	 */
	public Object getObject() {
		// FIXME ne fonctionne pas pour les property path à plus de 1 niveau
		if (this.simpleSource != null) {
			return this.simpleSource;
		} else {
			return this.source;
		}
	}

	public void fireError(DataMessage message, Handler handler) {
		if (this.feedback != null) {
			if (this.feedbackId != null) {
				message.setMessage(message.getMessage() + " : "
						+ this.feedbackId);
			}
			this.feedback.enterErrorState(message, handler);
		}
	}

	public void fireErrorStop(Handler handler) {
		if (this.feedback != null) {
			this.feedback.leaveErrorState(handler);
		}
	}

	public void setFeedbackManager(FeedbackManager manager, String feedbackId) {
		this.setFeedbackManager(manager);
		this.feedbackId = feedbackId;
	}

	public void setHandler(Handler handler) {
		this.handler = handler;
	}

	public Handler getHandler() {
		return this.handler;
	}

	public Widget getWidget() {
		return this.handler == null ? null : this.handler.getWidget();
	}

	public DataMessage getCurrentValidationState() {
		return this.currentError;
	}

	public DataMessage validate() {
		try {
			Object result = this.converter.fromStringToObject(this.handler
					.getValue().toString());
			this.validate(result);
			return null;

		} catch (ValidatorException ve) {
			if (this.feedback != null) {
				if (this.feedbackId != null && ve.getDataMessage() != null) {
					ve.getDataMessage().setMessage(
							ve.getDataMessage().getMessage() + " : "
									+ this.feedbackId);
				}
			}
			return ve.getDataMessage();

		} catch (ConverterException ce) {
			if (this.feedback != null) {
				if (this.feedbackId != null && ce.getDataMessage() != null) {
					ce.getDataMessage().setMessage(
							ce.getDataMessage().getMessage() + " : "
									+ this.feedbackId);
				}
			}
			return ce.getDataMessage();
		}
	}

}
